/*******************************************************************************
 * Copyright 2012 Erik Gast
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package easyobserving;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

public class DefaultObserverHandler<T> implements ObserverHandler<T>, ObserverRegistrable<T> {
	private final Class<T> observerClass;
	
	private final List<T> listeners = Collections.synchronizedList(new LinkedList<T>());

	/**
	 * @param observerClass the observer class type
	 */
	public DefaultObserverHandler(Class<T> observerClass) {
		this.observerClass = observerClass;
	}
	
	/* (non-Javadoc)
	 * @see test.IObserverHandler#addListener(T)
	 */
	@Override
	public void addObserver(T listener){
		listeners.add(listener);
	}

	/* (non-Javadoc)
	 * @see test.IObserverHandler#removeListener(T)
	 */
	@Override
	public void removeObserver(T listener){
		listeners.remove(listener);
	}
	
	/* (non-Javadoc)
	 * @see observerable.ObserverHandler#notifyAllObservers(java.lang.String, java.lang.Object[])
	 */
	@Override
	public void notifyObservers(String methodName, Object... args){
		Method method = findMethod(methodName, args);
		if(method != null){
			synchronized (listeners) {
				for(T listener: listeners){
					try {
						method.invoke(listener, args);
					} catch (SecurityException e) {
						throw new RuntimeException(e);
					} catch (IllegalArgumentException e) {
						throw new RuntimeException(e);
					} catch (IllegalAccessException e) {
						throw new RuntimeException(e);
					} catch (InvocationTargetException e) {
						throw new RuntimeException(e);
					}
				}
			}
		}else{
			StringBuilder msgBuilder = new StringBuilder();
			msgBuilder.append(observerClass + " has no method \""+methodName +"\"");
			if(args.length > 0){
				msgBuilder.append(" accepting "+Arrays.deepToString(getClassesOfArgs(args)));
			}else{
				msgBuilder.append(" with no parameters");
			}
			msgBuilder.append(" not found");
			throw new RuntimeException(msgBuilder.toString());			
		}
	}
	
	private Method findMethod(String methodName, Object... args){
		if(args == null){
			try {
				return observerClass.getMethod(methodName);
			} catch (SecurityException e) {
				throw new RuntimeException(e);
			} catch (NoSuchMethodException e) {
				throw new RuntimeException(e);
			}
		}else{
			Method[] methods = observerClass.getMethods();
			for(Method method: methods){
				if(method.getName().equals(methodName)){
					Class<?>[] methodTypes = method.getParameterTypes();
					if(methodTypes.length == args.length){
	
						boolean methodFound = true;
						for(int i=0;i<methodTypes.length && methodFound;i++){
							Object arg = args[i];
							if(arg != null){
								if(methodTypes[i].isPrimitive()){
									Class<?> correspondingClass = primitiveToClassMap.get(methodTypes[i].getName());
									if(correspondingClass == null || !correspondingClass.equals(args[i].getClass()) ){
										methodFound = false;
									}
								}else if(!methodTypes[i].isAssignableFrom(args[i].getClass())){
									methodFound = false;
								}
							}
						}
						if(methodFound){
							return method;
						}
					}
				}
			}
		}
		
		return null;
	}
	
	private Class<?>[] getClassesOfArgs(Object... args){
		Class<?>[] classes = new Class[args.length];
		for(int i=0;i<args.length;i++){
			Object arg = args[i];
			if(arg != null){
				classes[i] = args[i].getClass();
			}else{
				classes[i] = new Object().getClass();
			}
		}
		return classes;
	}

	/* (non-Javadoc)
	 * @see observerable.ObserverHandler#getNumObservers()
	 */
	@Override
	public int getObserverCount() {
		return listeners.size();
	}

	/* (non-Javadoc)
	 * @see observerable.ObserverHandler#isObserving(java.lang.Object)
	 */
	@Override
	public boolean isObserving(T observer) {
		return listeners.contains(observer);
	}
	
	/* (non-Javadoc)
	 * @see observerable.ObserverHandler#getObservers()
	 */
	@Override
	public List<T> getObservers() {
		return listeners;
	}
	
	@Override
	@SuppressWarnings("unchecked")
	public T getNotifyAllObserversProxy(){
		InvocationHandler handler = new InvocationHandler(){

			@Override
			public Object invoke(Object proxy, Method method, Object[] args)
					throws Throwable {
				synchronized (listeners) {
					for(T listener: listeners){
						method.invoke(listener, args);
					}
				}				
				return null;
			}
			
		};
		return(T) Proxy.newProxyInstance(observerClass.getClassLoader(), new Class[]{observerClass}, handler);
	}
	
	private static final Map<String, Class<?>> primitiveToClassMap = new HashMap<String, Class<?>>();{
		primitiveToClassMap.put("int", Integer.class );
		primitiveToClassMap.put("long", Long.class );
		primitiveToClassMap.put("double", Double.class );
		primitiveToClassMap.put("float", Float.class );
		primitiveToClassMap.put("bool", Boolean.class );
		primitiveToClassMap.put("char", Character.class );
		primitiveToClassMap.put("byte", Byte.class );
		primitiveToClassMap.put("void", Void.class );
		primitiveToClassMap.put("short", Short.class );
	}
}
